使用NIfTI图像
本页介绍NIfTI格式的nibabel实现的一些功能。一般来说,所有这些特征同样适用于NIfTI 1和NIfTI 2格式,但是我们会注意到它们之间的区别。NIfTI 1比NIfTI 2更普遍。
预赛
我们首先设置一些显示参数,以紧凑的形式打印numpy数组:
>>> import numpy as np
>>> # Set numpy to print only 2 decimal digits for neatness
>>> np.set_printoptions(precision=2, suppress=True)
NIfTI图像示例
>>> import os
>>> import nibabel as nib
>>> from nibabel.testing import data_path
这是NIfTI 1图像的例子:
>>> example_ni1 = os.path.join(data_path, 'example4d.nii.gz')
>>> n1_img = nib.load(example_ni1)
>>> n1_img
<nibabel.nifti1.Nifti1Image object at ...>
这里是NIfTI 2示例图像:
>>> example_ni2 = os.path.join(data_path, 'example_nifti2.nii.gz')
>>> n2_img = nib.load(example_ni2)
>>> n2_img
<nibabel.nifti2.Nifti2Image object at ...>
该NIfTI头
NIfTI 1头是一个352字节的小C结构。它包含以下字段:
>>> n1_header = n1_img.header
>>> print(n1_header)
<class 'nibabel.nifti1.Nifti1Header'> object, endian='<'
sizeof_hdr : 348
data_type : b''
db_name : b''
extents : 0
session_error : 0
regular : b'r'
dim_info : 57
dim : [ 4 128 96 24 2 1 1 1]
intent_p1 : 0.0
intent_p2 : 0.0
intent_p3 : 0.0
intent_code : none
datatype : int16
bitpix : 16
slice_start : 0
pixdim : [ -1. 2. 2. 2.2 2000. 1. 1. 1. ]
vox_offset : 0.0
scl_slope : nan
scl_inter : nan
slice_end : 23
slice_code : unknown
xyzt_units : 10
cal_max : 1162.0
cal_min : 0.0
slice_duration : 0.0
toffset : 0.0
glmax : 0
glmin : 0
descrip : b'FSL3.3\x00 v2.25 NIfTI-1 Single file format'
aux_file : b''
qform_code : scanner
sform_code : scanner
quatern_b : -1.94510681403e-26
quatern_c : -0.996708512306
quatern_d : -0.081068739295
qoffset_x : 117.855102539
qoffset_y : -35.7229423523
qoffset_z : -7.24879837036
srow_x : [ -2. 0. 0. 117.86]
srow_y : [ -0. 1.97 -0.36 -35.72]
srow_z : [ 0. 0.32 2.17 -7.25]
intent_name : b''
magic : b'n+1'
NIfTI 2头是相似的,但长度为540字节,字段较少:
>>> n2_header = n2_img.header
>>> print(n2_header)
<class 'nibabel.nifti2.Nifti2Header'> object, endian='<'
sizeof_hdr : 540
magic : b'n+2'
eol_check : [13 10 26 10]
datatype : int16
bitpix : 16
dim : [ 4 32 20 12 2 1 1 1]
intent_p1 : 0.0
intent_p2 : 0.0
intent_p3 : 0.0
pixdim : [ -1. 2. 2. 2.2 2000. 1. 1. 1. ]
vox_offset : 0
scl_slope : nan
scl_inter : nan
cal_max : 1162.0
cal_min : 0.0
slice_duration : 0.0
toffset : 0.0
slice_start : 0
slice_end : 23
descrip : b'FSL3.3\x00 v2.25 NIfTI-1 Single file format'
aux_file : b''
qform_code : scanner
sform_code : scanner
quatern_b : -1.94510681403e-26
quatern_c : -0.996708512306
quatern_d : -0.081068739295
qoffset_x : 117.855102539
qoffset_y : -35.7229423523
qoffset_z : -7.24879837036
srow_x : [ -2. 0. 0. 117.86]
srow_y : [ -0. 1.97 -0.36 -35.72]
srow_z : [ 0. 0.32 2.17 -7.25]
slice_code : unknown
xyzt_units : 10
intent_code : none
intent_name : b''
dim_info : 57
unused_str : b''
您可以使用dict(映射类型)项访问来获取和设置标题中的单个字段。例如:
>>> n1_header['cal_max']
array(1162.0, dtype=float32)
>>> n1_header['cal_max'] = 1200
>>> n1_header['cal_max']
array(1200.0, dtype=float32)
检查get_
/set_
方法头部的属性以获取和设置NIfTI头部字段的各种组合。
该get_
/set_
方法应该从头部校验和应用价值的有效组合,而你可以做任何你用字典/映射项目的访问一样。只有当/方法不会做你想要的时候,使用get_
/set_
方法和使用映射项访问更安全。get_set_
NIfTI赞同
像其他nibabel图像类型一样,NIfTI图像具有将体素坐标与RAS +空间中的世界坐标相关的仿射:
>>> n1_img.affine
array([[ -2. , 0. , 0. , 117.86],
[ -0. , 1.97, -0.36, -35.72],
[ 0. , 0.32, 2.17, -7.25],
[ 0. , 0. , 0. , 1. ]])
与其他格式不同,该NIfTI报头格式可以指定该仿射以三种方式之一-sform仿射的qform仿射和回退头仿射。
Nibabel使用算法来选择这三个中的哪一个用于整个图像affine
。
表格仿射
头存储所述4乘4仿射前三行中的报头字段srow_x
,srow_y
,srow_z
。标题不会存储第四行,因为它始终是(请参阅坐标系和仿射)。[0,0,0,1]
您可以使用get_sform()
图像或标题的方法来获得特定的仿射。
例如:
>>> print(n1_header['srow_x'])
[ -2. 0. 0. 117.86]
>>> print(n1_header['srow_y'])
[ -0. 1.97 -0.36 -35.72]
>>> print(n1_header['srow_z'])
[ 0. 0.32 2.17 -7.25]
>>> print(n1_header.get_sform())
[[ -2. 0. 0. 117.86]
[ -0. 1.97 -0.36 -35.72]
[ 0. 0.32 2.17 -7.25]
[ 0. 0. 0. 1. ]]
这个仿射只有在sform_code
不为零时才有效。
>>> print(n1_header['sform_code'])
1
不同的表单代码值指定了表单仿射引用的RAS +空间,具有以下解释:
码 | 标签 | 含义 |
---|---|---|
0 | 未知 | sform没有定义 |
1 | 扫描器 | RAS +在扫描仪坐标 |
2 | 对齐 | RAS +与其他扫描对齐 |
3 | 塔莱拉什 | RAS +在Talairach地图集空间 |
4 | MNI | RAS +在MNI地图集空间 |
在我们的例子中,代码是1,意思是“扫描仪”对齐。
你可以使用coded=True
参数得到仿射和代码get_sform()
:
>>> print(n1_header.get_sform(coded=True))
(array([[ -2. , 0. , 0. , 117.86],
[ -0. , 1.97, -0.36, -35.72],
[ 0. , 0.32, 2.17, -7.25],
[ 0. , 0. , 0. , 1. ]]), array(1, dtype=int16))
您可以使用set_sform()
标题和图像的方法设置sform。
>>> n1_header.set_sform(np.diag([2, 3, 4, 1]))
>>> n1_header.get_sform()
array([[ 2., 0., 0., 0.],
[ 0., 3., 0., 0.],
[ 0., 0., 4., 0.],
[ 0., 0., 0., 1.]])
使用code
参数设置仿射和代码set_sform()
:
>>> n1_header.set_sform(np.diag([3, 4, 5, 1]), code='mni')
>>> n1_header.get_sform(coded=True)
(array([[ 3., 0., 0., 0.],
[ 0., 4., 0., 0.],
[ 0., 0., 5., 0.],
[ 0., 0., 0., 1.]]), array(4, dtype=int16))
qform仿射
该仿射可以从体素尺寸的组合来计算值(条目1穿过的4pixdim
场),被称为符号翻转qfac
存储在条目0pixdim
,和一个四元数,可以从字段被重建quatern_b
,quatern_c
,quatern_d
。
有关详细信息,请参阅代码。get_qform()method
您可以获取和设置使用等效方法来者为sform的qform仿射:get_qform()
,set_qform()
。
>>> n1_header.get_qform(coded=True)
(array([[ -2. , 0. , 0. , 117.86],
[ -0. , 1.97, -0.36, -35.72],
[ 0. , 0.32, 2.17, -7.25],
[ 0. , 0. , 0. , 1. ]]), array(1, dtype=int16))
qform也qform_code
与sform_code具有相同的解释。
后退标题
这是最后的仿制品,只从pixdim
体素大小构造。该NIfTI规范说,这应该设置第一体素的图像中[0,0,0]在世界坐标,但我们nibabblers遵循SPM中宁愿设置中心体素具有[0,0,0]世界坐标。NIfTI规范还暗示图像应该被假定为对于这个仿射的RAS +体素方向(参见坐标系和仿射)。再次像SPM一样,我们宁愿默认假设LAS +体素方向。
你总是可以得到与后面的仿效get_base_affine()
:
>>> n1_header.get_base_affine()
array([[ -2. , 0. , 0. , 127. ],
[ 0. , 2. , 0. , -95. ],
[ 0. , 0. , 2.2, -25.3],
[ 0. , 0. , 0. , 1. ]])
选择图像仿射
鉴于在NIfTI头文件中定义了三个可能的仿射关系,nibabel必须选择其中哪一个用于图像affine
。
该算法在该get_best_affine()
方法中被定义。它是:
- 如果
sform_code
!= 0('unknown')使用sform仿射; 其他 - 如果
qform_code
!= 0('unknown')使用qform仿射; 其他 - 使用倒退仿射。
默认的sform和qform代码
如果您创建一个新的图像,例如:
>>> data = np.random.random((20, 20, 20))
>>> xform = np.eye(4) * 2
>>> img = nib.nifti1.Nifti1Image(data, xform)
sform和qform代码将分别初始化为2(对齐)和0(未知):
>>> img.get_sform(coded=True)
(array([[ 2., 0., 0., 0.],
[ 0., 2., 0., 0.],
[ 0., 0., 2., 0.],
[ 0., 0., 0., 1.]]), array(2, dtype=int16))
>>> img.get_qform(coded=True)
(None, 0)
这是基于假定您为新创建的图像指定的仿射图将使图像与某个已知的坐标系对齐。根据NIfTI规范,qform旨在将转换编码到扫描仪坐标中 - 对于以编程方式创建的图像,我们无法知道扫描仪坐标系是什么;此外,qform不能用来存储任意的仿射变换,因为它不能编码剪切。所以提供的仿射将存储在sform中,并且qform将保持未初始化状态。
如果您创建一个新的图像并指定一个现有的标题,例如:
>>> example_ni1 = os.path.join(data_path, 'example4d.nii.gz')
>>> n1_img = nib.load(example_ni1)
>>> new_header = header=n1_img.header.copy()
>>> new_data = np.random.random(n1_img.shape[:3])
>>> new_img = nib.nifti1.Nifti1Image(data, None, header=new_header)
那么新创建的图像将继承提供的标题中的相同的sform和qform代码。但是,如果您用指定的仿射和标题创建新图像,例如:
>>> xform = np.eye(4)
>>> new_img = nib.nifti1.Nifti1Image(data, xform, header=new_header)
那么如果提供的仿射与提供的头部中的仿射相同,则只保留sform和qform代码。如果仿射不匹配,则sform和qform代码将分别设置为默认值2和0。这样做的基础是,如果你正在改变仿射,你很可能会改变仿射所指向的空间。所以原来的sform和qform代码不能再被认为是有效的。
如果您希望将sform和qform仿射和/或代码设置为其他值,则可以在创建之后使用set_sform
和set_qform
方法始终设置它们,如上所述。
数据缩放
NIfTI使用简单的数据缩放方案。
默认情况下,nibabel将为您处理这种缩放,但是您可能有时想自己控制数据缩放。如果是这样,下一节将描述缩放的工作原理和nibabel的实现。
标题中有两个缩放字段,分别叫做scl_slope
和scl_inter
。
NIfTI图像的输出数据来自:
- 从图像文件中加载二进制数据;
- 将数字转换为标题中给出的二进制格式并返回
get_data_dtype()
; - 重塑输出图像形状;
scl_slope
如果两者scl_slope
和scl_inter
都定义 ,则将 结果乘以标题 值 ;- 头值添加
scl_inter
值的结果,如果两者scl_slope
和scl_inter
定义;
“定义”的意思是,价值不是NaN(不是数字)。
当你加载一个NIfTI图像时,所有这些都被内置到数组代理中。
加载图像时,标题缩放值会自动设置为NaN(未定义),以标记缩放值已被读取消耗的事实。从加载头读取的缩放值仅出现在数组代理对象中。
要看看这是如何工作的,让我们用一些缩放比例来制作一个新的图像:
>>> array_data = np.arange(24, dtype=np.int16).reshape((2, 3, 4))
>>> affine = np.diag([1, 2, 3, 1])
>>> array_img = nib.Nifti1Image(array_data, affine)
>>> array_header = array_img.header
默认缩放值是NaN(未定义):
>>> array_header['scl_slope']
array(nan, dtype=float32)
>>> array_header['scl_inter']
array(nan, dtype=float32)
您可以使用以下get_slope_inter()
方法获取缩放值:
>>> array_header.get_slope_inter()
(None, None)
没有对应于NaN标度值(未定义)。
我们可以将它们设置在图像标题中,以便在写入图像时将它们保存到标题中。我们可以通过直接设置字段,或者通过以下方式来实现set_slope_inter()
:
>>> array_header.set_slope_inter(2, 10)
>>> array_header.get_slope_inter()
(2.0, 10.0)
>>> array_header['scl_slope']
array(2.0, dtype=float32)
>>> array_header['scl_inter']
array(10.0, dtype=float32)
在我们保存并重新加载之前,设置标题中的比例因子对图像数据没有影响:
>>> array_img.get_data()
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]], dtype=int16)
现在我们保存图像并重新加载:
>>> nib.save(array_img, 'scaled_image.nii')
>>> scaled_img = nib.load('scaled_image.nii')
数据数组应用了缩放:
>>> scaled_img.get_data()
...([[[ 10., 12., 14., 16.],
[ 18., 20., 22., 24.],
[ 26., 28., 30., 32.]],
[[ 34., 36., 38., 40.],
[ 42., 44., 46., 48.],
[ 50., 52., 54., 56.]]])
已加载图像的标题已将缩放重置为未定义,以标记缩放已被负载“消耗”的事实:
>>> scaled_img.header.get_slope_inter()
(None, None)
数组代理对象中仍然可以访问原始斜率和截距:
>>> scaled_img.dataobj.slope
2.0
>>> scaled_img.dataobj.inter
10.0
如果在保存图像时头部缩放不确定,nibabel会尝试找到最佳的斜率和截距,以最好地保持输出数据类型中数据的精度。由于nibabel会在加载图像时将缩放比例设置为undefined或者创建新图像,因此这是默认行为。